#include <sobjc-commons/defines.h>
#include <sobjc-commons/utils.h>
#include <sobjc-commons/st.h>
#include "sobjc/runtime.h"
#include "sobjc/proto.h"
#include "sobjc/protocol.h"
#include "ivar.h"
#include "method.h"
#include "selector.h"

static BOOL inited = NO;
static ClassList class_list = NULL;
static st_table *class_table;
static st_table_t proto_class_table = NULL;
static CategoryList cat_list = NULL;
static objc_lookup_class_f lookup_class_callback = NULL;

static Class make_class(ProtoClass);
static void add_proto_methods_to_class(int, struct objc_proto_method *, Class);
static int lookup_imp_error(id recv, SEL sel, ...);

#ifdef OBJC_PROFILE
struct objc_cache_profile {
  unsigned long hit_count;
  unsigned long miss_count;
  unsigned long dup_count;
  unsigned long flush_count;
};

static struct objc_cache_profile cache_profile;
static void print_profile();
#endif /* OBJC_PROFILE */

#define CACHE_SIZE              4096
#define CACHE_MASK              (CACHE_SIZE-1)
#define CACHE_HASH(cls,sel)     ((((size_t)(cls)>>3) ^ (size_t)(sel)) & CACHE_MASK)

static Method meth_cache[CACHE_SIZE];
static Method super_meth_cache[CACHE_SIZE];

void objc_init_runtime()
{
  if (inited)
    return;
  else {
    objc_init_sel();
    class_table = st_init_strtable();
    proto_class_table = st_init_strtable();
    MEMZERO(meth_cache, Method, CACHE_SIZE);
    MEMZERO(super_meth_cache, Method, CACHE_SIZE);

#ifdef OBJC_PROFILE
    MEMZERO(&cache_profile, struct objc_cache_profile, 1);
    atexit(print_profile);
#endif /* OBJC_PROFILE */

    inited = YES;
  }
}

void
objc_register_proto_class(ProtoClass cls)
{
  st_add_direct(proto_class_table, (st_data_t)cls->name, (st_data_t)cls);
}

ClassList
objc_get_class_list()
{
  return class_list;
}

Class
objc_lookup_class(const char *name)
{
  Class cls = NULL;
  ProtoClass proto;

  if (!st_lookup(class_table, (st_data_t)name, (st_data_t *)&cls)) {
    if (st_lookup(proto_class_table, (st_data_t)name, (st_data_t *)&proto)) {
      cls = make_class(proto);
      objc_add_class(cls);
    } else if (lookup_class_callback != NULL)
      cls = lookup_class_callback(name);
  }
  return cls;
}

static
Class make_class(ProtoClass proto)
{
  Class cls, superclass = NULL;
  size_t inst_size = 0;
  IvarList ivars;
  Ivar ivar;
  ProtocolList protocols = NULL;
  int i;

  /* superclass */
  if (proto->super_name != NULL)
    superclass = objc_lookup_class(proto->super_name);

  /* protocols */

  /* instance variables */
  for (i = 0; i < proto->nivars; i++) {
    ivar = objc_make_ivar(proto->ivars[i].name,
                          proto->ivars[i].type,
                          (ptrdiff_t)inst_size);
    ivars = objc_ivar_list_add(ivars, ivar);
    inst_size += proto->ivars[i].size;
  }

  cls = objc_make_class(proto->name, superclass, inst_size,
                        ivars, protocols, proto->ninst_dcodes);
  cls->isa = objc_make_metaclass(cls, proto->nclass_dcodes);

  /* methods */
  add_proto_methods_to_class(proto->ninst_meths,
                             proto->inst_meths, cls);
  add_proto_methods_to_class(proto->nclass_meths,
                             proto->class_meths, cls->isa);

  return cls;
}

static void
add_proto_methods_to_class(int nmeths,
                           struct objc_proto_method *proto_meths,
                           Class cls)
{
  Method meth;
  int i;

  for (i = 0; i < nmeths; i++) {
    meth = objc_make_method(proto_meths[i].name,
                            proto_meths[i].types,
                            proto_meths[i].imp);
    objc_class_add_method(cls, meth, proto_meths[i].dcode);
  }
}

void
objc_set_lookup_class_callback(objc_lookup_class_f callback)
{
  lookup_class_callback = callback;
}

void
objc_add_class(Class cls)
{
  class_list = objc_class_list_add(class_list, cls);
  st_add_direct(class_table, (st_data_t)cls->name, (st_data_t)cls);
}

void
objc_register_category(Category cat)
{
  /* FIXME */
  cat_list = objc_category_list_add(cat_list, cat);
}

IMP
objc_lookup_imp(id recv, SEL sel)
{
  Method meth;
  Class cls;

  if (recv == nil)
    return nil;

  cls = recv->isa;
  meth = meth_cache[CACHE_HASH(cls, sel)];
  if (meth != NULL && meth->meth_class == cls) {
#ifdef OBJC_PROFILE
    cache_profile.hit_count++;
    sel->send_count++;
#endif /* OBJC_PROFILE */
    return meth->imp;
  }

#ifdef OBJC_PROFILE
  if (meth != NULL)
    cache_profile.dup_count++;
#endif /* OBJC_PROFILE */

  do {
    meth = objc_class_get_instance_method(cls, sel);
    if (meth != NULL) {
      /* register cache for receiver class */
      meth_cache[CACHE_HASH(recv->isa, sel)] = meth;
#ifdef OBJC_PROFILE
      cache_profile.miss_count++;
#endif /* OBJC_PROFILE */
      return meth->imp;
    } else {
      cls = cls->superclass;
    }
  } while (cls != NULL);
  return (IMP)lookup_imp_error;
}

IMP
objc_lookup_imp_super(Class superclass, id recv, SEL sel)
{
  Method meth;
  Class cls;

  if (recv == nil)
    return nil;

  cls = superclass;
  do {
    meth = super_meth_cache[CACHE_HASH(cls, sel)];
    if (meth != NULL && meth->meth_class == cls)
      return meth->imp;
    else {
      meth = objc_class_get_instance_method(cls, sel);
      if (meth != NULL) {
        /* register cache for receiver superclass */
        super_meth_cache[CACHE_HASH(superclass, sel)] = meth;
        return meth->imp;
      }
      cls = cls->superclass;
    }
  } while (cls != NULL);
  return (IMP)lookup_imp_error;
}

static int
lookup_imp_error(id recv, SEL sel, ...)
{
  printf("cannot lookup message %s for %s\n", sel->name, recv->isa->name);
  return 0;
}

id
objc_nil_return(id recv __attribute__((unused)),
                SEL sel __attribute__((unused)), ...)
{
  return nil;
}

id
objc_fail_dispatch(id recv, SEL sel, ...)
{
  printf("cannot dispatch '%s' for '%s'\n", sel->name, recv->isa->name);
  return nil;
}

#ifdef OBJC_PROFILE

static void
print_profile()
{
  unsigned long total;

  /* cache profile */
  total = cache_profile.hit_count + cache_profile.miss_count;
  printf("Method cache:\n");
  printf("    Total lookup:     %lu\n", total);
  printf("    Lookup success:   %lu (%.2f%%)\n", cache_profile.hit_count,
         (double)cache_profile.hit_count / total * 100);
  printf("    Lookup miss:      %lu (%.2f%%)\n", cache_profile.miss_count,
         (double)cache_profile.miss_count / total * 100);
  printf("    Duplicate:        %lu (%.2f%%)\n", cache_profile.dup_count,
         (double)cache_profile.dup_count / total * 100);
}

#endif /* OBJC_PROFILE */

